package IC.SemanticChecks;

import java.util.ArrayList;
import java.util.List;

import IC.SemanticError;
import IC.AST.ArrayLocation;
import IC.AST.Assignment;
import IC.AST.Break;
import IC.AST.Call;
import IC.AST.CallStatement;
import IC.AST.Continue;
import IC.AST.Expression;
import IC.AST.ExpressionBlock;
import IC.AST.Field;
import IC.AST.Formal;
import IC.AST.ICClass;
import IC.AST.If;
import IC.AST.Length;
import IC.AST.LibraryMethod;
import IC.AST.Literal;
import IC.AST.LocalVariable;
import IC.AST.Location;
import IC.AST.LogicalBinaryOp;
import IC.AST.LogicalUnaryOp;
import IC.AST.MathBinaryOp;
import IC.AST.MathUnaryOp;
import IC.AST.Method;
import IC.AST.NewArray;
import IC.AST.NewClass;
import IC.AST.PrimitiveType;
import IC.AST.Program;
import IC.AST.Return;
import IC.AST.Statement;
import IC.AST.StatementsBlock;
import IC.AST.StaticCall;
import IC.AST.StaticMethod;
import IC.AST.This;
import IC.AST.UserType;
import IC.AST.VariableLocation;
import IC.AST.VirtualCall;
import IC.AST.VirtualMethod;
import IC.AST.Visitor;
import IC.AST.While;
import IC.Symbols.BlockSymbolTable;
import IC.Symbols.ClassSymbol;
import IC.Symbols.ClassSymbolTable;
import IC.Symbols.GlobalSymbolTable;
import IC.Symbols.MethodSymbol;
import IC.Symbols.MethodSymbolTable;
import IC.Symbols.Symbol;
import IC.Symbols.SymbolTable;
import IC.Types.ClassType;
import IC.Types.MethodType;
import IC.Types.Type;
import IC.Types.TypeTable;

public class TablesConstructorVisitor implements Visitor {

	private static String STATEMENT_BLOCK_SYMBOLTABLE_PREFIX = "statement block in ";

	protected GlobalSymbolTable gst;
	protected ICClass libraryClass;
	protected int appearanceOrder;

	/**
	 * Constructs a Type table and Symbol table
	 * 
	 */
	public TablesConstructorVisitor(GlobalSymbolTable gst, ICClass libraryClass) {
		this.gst = gst;
		this.libraryClass = libraryClass;
		this.appearanceOrder = 0; 
	}

	public Object visit(Program program) {

		program.setEnclosingScope(gst);

		MethodSymbol mainMethodSymbol = null;
		ICClass classContainingMainMethodSymbol = null;
		
		if (libraryClass != null){
			if (!libraryClass.getName().equals("Library")){
				System.err.println(new SemanticError(libraryClass.getLine(), "class name should be Library"));
				return null;
			}
			if (libraryClass.hasSuperClass()){
				System.err.println(new SemanticError(libraryClass.getLine(), "Library class should not extend any other class"));
				return null;
			}
			program.getClasses().add(0, libraryClass);
		}
		
		List<ICClass> classes = program.getClasses();

		for (ICClass currentClass: classes){

			String className = currentClass.getName();
			String superClassName = currentClass.getSuperClassName();
			
			ClassType classType = TypeTable.classType(className, superClassName, currentClass);
			if (classType == null){
				System.err.println(String.format("semantic error at line %s: Class %s cannot extend the undeclared class %s. " +
						"Classes can only extend previously defined classes. ", currentClass.getLine(), className, superClassName));
				return null;
			}
		}

		for (ICClass currentClass: classes){
			try {
				gst.insert(currentClass);
			} catch (SemanticError e) {
				System.err.println(e);
				return null;
			}
		}
		
		for (ICClass currentClass : classes){
			SymbolTable parentSymbolTable;
			if (currentClass.hasSuperClass()){
				parentSymbolTable = getClassSymbolTable((ClassSymbol) gst.lookup(currentClass.getSuperClassName()));
			}
			else{
				parentSymbolTable = gst;
			}

			currentClass.setEnclosingScope(parentSymbolTable);

			ClassSymbolTable cst = (ClassSymbolTable) currentClass.accept(this);
			if (cst == null){
				return null;
			}

			parentSymbolTable.addChild(cst.getId(), cst);

			//Check if a main method has been declared in this class
			Symbol currentMainSymbol = cst.lookup("main", false);
			if ((currentMainSymbol != null && currentMainSymbol instanceof MethodSymbol) && (((MethodSymbol) currentMainSymbol).isStatic())){
				MethodSymbol currentMainMethodSymbol = (MethodSymbol) currentMainSymbol;
				MethodType type = (MethodType) currentMainMethodSymbol.getType();
				if (type.getReturnType() == TypeTable.getVoidType()){
					Type[] paramTypes = type.getParamTypes();
					if (paramTypes.length == 1){
						if (paramTypes[0] == TypeTable.arrayType(TypeTable.getStringType())){
							if (mainMethodSymbol != null){
								System.err.println(new SemanticError(program.getLine(), "duplication of a main method with the signature: " +
										"\"static void main (string[] args)\" in classes " + currentClass.getName() + 
										" and " + classContainingMainMethodSymbol.getName()));
								return null;
							}
							else{
								mainMethodSymbol = currentMainMethodSymbol;
								classContainingMainMethodSymbol = currentClass;
							}
						}
					}
				}
			}
		}			

		//Check if a main method has been declared in this program
		if (mainMethodSymbol == null){
			System.err.println(new SemanticError(program.getLine(), "program " + gst.getId() + " must have exactly one method main, with the following signature: \"static void main (string[] args)\""));
			return null;
		}
		else{
			return gst;
		}
	}

	public Object visit(ICClass icClass) {
		ClassSymbolTable cst = new ClassSymbolTable(icClass.getName(), icClass.getEnclosingScope());
		List<Field> fields = icClass.getFields();

		for (Field currentField: fields){
			try {
				currentField.setEnclosingScope(cst);
				cst.insert(currentField);
				if (currentField.accept(this) == null){
					return null;
				}
			} catch (SemanticError e) {
				System.err.println(e);
				return null;
			}
		}

		List<Method> methods = icClass.getMethods();
		for (Method currentMethod: methods){
			currentMethod.setEnclosingScope(cst);
			if (currentMethod instanceof VirtualMethod){
				try {
					cst.insert((VirtualMethod) currentMethod);
				} 
				catch (SemanticError e) {
					System.err.println(e);
					return null;
				}
			}
			else if (currentMethod instanceof StaticMethod){
				try {
					cst.insert((StaticMethod) currentMethod);
				} 
				catch (SemanticError e) {
					System.err.println(e);
					return null;
				}
			}
			else{
				try {
					cst.insert((LibraryMethod) currentMethod);
				} 
				catch (SemanticError e) {
					System.err.println(e);
					return null;
				}
			}


			MethodSymbolTable mst = (MethodSymbolTable) currentMethod.accept(this);
			if (mst == null){
				return null;
			}
			cst.addChild(mst.getId(), mst);
		}

		return cst;
	}

	public Object visit(PrimitiveType type) {
		return true;
	}

	public Object visit(UserType type) {
		try {
			TypeTable.getType(type);
		} catch (SemanticError e) {
			System.err.println(e);
			return null;
		}
		return true;
	}

	public Object visit(Field field) {
		try {
			field.getType().setEnclosingScope(field.getEnclosingScope());
			return TypeTable.getType(field);
		} catch (SemanticError e) {
			System.err.println(e);
			return null;
		}

	}

	public Object visit(LibraryMethod method) {
		return visitMethod(method, true);
	}

	public Object visit(Formal formal) {
		try {
			formal.getType().setEnclosingScope(formal.getEnclosingScope());
			return TypeTable.getType(formal);
		} catch (SemanticError e) {
			System.err.println(e);
			return null;
		}
	}

	public Object visit(VirtualMethod method) {
		return visitMethod(method, false);	
	}

	public Object visit(StaticMethod method) {
		return visitMethod(method, true);
	}

	public Object visit(Assignment assignment) {
		Expression assignmentExpr = assignment.getAssignment();
		assignmentExpr.setEnclosingScope(assignment.getEnclosingScope());
		if (assignmentExpr.accept(this) == null){
			return null;
		}

		Expression locationVariableExpr = assignment.getVariable();
		locationVariableExpr.setEnclosingScope(assignment.getEnclosingScope());

		if (visitLocation((Location) locationVariableExpr) == null){
			return null;
		}
		return true;
	}

	public Object visit(CallStatement callStatement) {
		Call call = callStatement.getCall();
		call.setEnclosingScope(callStatement.getEnclosingScope());
		if (call.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(Return returnStatement) {
		if (returnStatement.hasValue()){
			Expression returnExpr = returnStatement.getValue();
			returnExpr.setEnclosingScope(returnStatement.getEnclosingScope());
			if (returnExpr.accept(this) == null){
				return null;
			}
		}

		return true;
	}

	public Object visit(If ifStatement) {
		Expression cond = ifStatement.getCondition();
		cond.setEnclosingScope(ifStatement.getEnclosingScope());
		if (cond.accept(this) == null){
			return null;
		}

		Statement ifStmt = ifStatement.getOperation();
		ifStmt.setEnclosingScope(ifStatement.getEnclosingScope());
		ifStmt.setInsideALoop(ifStatement.isInsideALoop());
		if (ifStmt.accept(this) == null){
			return null;
		}

		if (ifStatement.hasElse()){
			Statement elseStatment = ifStatement.getElseOperation();
			elseStatment.setEnclosingScope(ifStatement.getEnclosingScope());
			elseStatment.setInsideALoop(ifStatement.isInsideALoop());
			if (elseStatment.accept(this) == null){
				return null;
			}
		}
		return true;
	}

	public Object visit(While whileStatement) {
		Expression cond = whileStatement.getCondition();
		cond.setEnclosingScope(whileStatement.getEnclosingScope());
		if (cond.accept(this) == null){
			return null;
		}

		Statement stmt = whileStatement.getOperation();
		stmt.setInsideALoop(true);

		if (stmt instanceof LocalVariable){
			String bstId = STATEMENT_BLOCK_SYMBOLTABLE_PREFIX + whileStatement.getEnclosingScope().getName();
			BlockSymbolTable bst = new BlockSymbolTable(bstId, whileStatement.getEnclosingScope());
			stmt.setEnclosingScope(bst);
		}
		else{
			stmt.setEnclosingScope(whileStatement.getEnclosingScope());
		}

		if (stmt.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(Break breakStatement) {
		if (!breakStatement.isInsideALoop()){
			System.err.println(new SemanticError(breakStatement.getLine(), "break statement should appear only inside loops"));
			return null;
		}
		return true;
	}

	public Object visit(Continue continueStatement) {
		if (!continueStatement.isInsideALoop()){
			System.err.println(new SemanticError(continueStatement.getLine(), "continue statement should appear only inside loops"));
			return null;
		}
		return true;	
	}

	public Object visit(StatementsBlock statementsBlock) {

		String statementBlockName = STATEMENT_BLOCK_SYMBOLTABLE_PREFIX + statementsBlock.getEnclosingScope().getName();
		BlockSymbolTable bst = new BlockSymbolTable(statementBlockName, statementsBlock.getEnclosingScope());

		List<Statement> statements = statementsBlock.getStatements();
		for (Statement currentStatement : statements){
			currentStatement.setEnclosingScope(bst);
			currentStatement.setInsideALoop(statementsBlock.isInsideALoop());
			Object result = currentStatement.accept(this);
			if (result == null){
				return null;
			}
		}

		statementsBlock.getEnclosingScope().addChild(bst.getId(), bst);
		return bst;
	}

	public Object visit(LocalVariable localVariable) {
		try {
			localVariable.getType().setEnclosingScope(localVariable.getEnclosingScope());
			TypeTable.getType(localVariable);
			if (localVariable.hasInitValue()){
				Expression initValueExpr = localVariable.getInitValue();
				initValueExpr.setEnclosingScope(localVariable.getEnclosingScope());
				if (initValueExpr.accept(this) == null){
					return null;
				}
			}
			localVariable.setOrder(appearanceOrder++);
			if (localVariable.getEnclosingScope() instanceof MethodSymbolTable){
				((MethodSymbolTable) localVariable.getEnclosingScope()).insert(localVariable);
			}
			else{
				((BlockSymbolTable) localVariable.getEnclosingScope()).insert(localVariable);
			}
			return true;
		} catch (SemanticError e) {
			System.err.println(e);
			return null;
		}
	}

	public Object visit(VariableLocation location) {
		location.setOrder(appearanceOrder++);
		if (location.isExternal()) {
			Expression locationExpr = location.getLocation();
			locationExpr.setEnclosingScope(location.getEnclosingScope());
			if (locationExpr.accept(this) == null){
				return null;
			}
		}

		return true;
	}


	public Object visit(ArrayLocation location) {
		Expression arrayExpr = location.getArray();
		arrayExpr.setEnclosingScope(location.getEnclosingScope());

		Expression indexExpr = location.getIndex();
		indexExpr.setEnclosingScope(location.getEnclosingScope());

		if (arrayExpr.accept(this) == null || indexExpr.accept(this) == null) {
			return null;
		}

		return true;
	}

	StringBuffer output = new StringBuffer();

	public Object visit(StaticCall call) {
		List<Expression> exprs = call.getArguments();
		for (Expression expr: exprs){
			expr.setEnclosingScope(call.getEnclosingScope());
			if (expr.accept(this) == null){
				return null;
			}
		}


		return true;
	}

	public Object visit(VirtualCall call) {
		if (call.isExternal()){
			Expression locationExpr = call.getLocation();
			locationExpr.setEnclosingScope(call.getEnclosingScope());
			if (locationExpr.accept(this) == null){
				return null;
			}
		}

		List<Expression> exprs = call.getArguments();
		for (Expression currentExpr : exprs){
			currentExpr.setEnclosingScope(call.getEnclosingScope());
			if (currentExpr.accept(this) == null){
				return null;
			}
		}

		return true;
	}

	public Object visit(This thisExpression) {
		//Check that this keyword is used only in instance method
		//if we're in a block we want to get to the first parent which is not block to test for static
		SymbolTable symbolTable = thisExpression.getEnclosingScope();
		while (symbolTable instanceof BlockSymbolTable){
			symbolTable = symbolTable.getParent();
		}
		//we reached the first method - check if its static
		if (((MethodSymbolTable) symbolTable).isStatic()) {
			System.err.println(new SemanticError(thisExpression.getLine(), "this keyword should be only used in instance methods"));
			return null;
		}
		
		while ((symbolTable instanceof MethodSymbolTable) && symbolTable != null){
			symbolTable = symbolTable.getParent();
		}

		
		if (symbolTable != null) {
			thisExpression.setEnclosingScope(symbolTable);
			return true;
		}
		else{
			System.err.println(new SemanticError(thisExpression.getLine(), "this keyword should be only used in instance methods"));
			return null;
		}
	}

	public Object visit(NewClass newClass) {

		if (!TypeTable.isClassDefined(newClass.getName())){
			return null;
		}

		return true;
	}

	public Object visit(NewArray newArray) {
		try {
			newArray.getType().setEnclosingScope(newArray.getEnclosingScope());
			TypeTable.getType(newArray);
		} catch (SemanticError e) {
			System.err.println(e);
			return null;
		}

		Expression sizeExpr = newArray.getSize();
		sizeExpr.setEnclosingScope(newArray.getEnclosingScope());
		if (sizeExpr.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(Length length) {
		Expression arrayExpr = length.getArray();
		arrayExpr.setEnclosingScope(length.getEnclosingScope());
		if (arrayExpr.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(MathBinaryOp binaryOp) {
		Expression firstOperandExpr = binaryOp.getFirstOperand();
		firstOperandExpr.setEnclosingScope(binaryOp.getEnclosingScope());
		if (firstOperandExpr.accept(this) == null){
			return null;
		}

		Expression secondOperandExpr = binaryOp.getSecondOperand();
		secondOperandExpr.setEnclosingScope(binaryOp.getEnclosingScope());
		if (secondOperandExpr.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(LogicalBinaryOp binaryOp) {

		Expression firstOperandExpr = binaryOp.getFirstOperand();
		firstOperandExpr.setEnclosingScope(binaryOp.getEnclosingScope());
		if (firstOperandExpr.accept(this) == null){
			return null;
		}

		Expression secondOperandExpr = binaryOp.getSecondOperand();
		secondOperandExpr.setEnclosingScope(binaryOp.getEnclosingScope());
		if (secondOperandExpr.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(MathUnaryOp unaryOp) {
		Expression operandExpr = unaryOp.getOperand();
		operandExpr.setEnclosingScope(unaryOp.getEnclosingScope());
		if (operandExpr.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(LogicalUnaryOp unaryOp) {
		Expression operandExpr = unaryOp.getOperand();
		operandExpr.setEnclosingScope(unaryOp.getEnclosingScope());
		if (operandExpr.accept(this) == null){
			return null;
		}

		return true;
	}

	public Object visit(Literal literal) {
		return true;
	}

	public Object visit(ExpressionBlock expressionBlock) {
		Expression expr = expressionBlock.getExpression();
		expr.setEnclosingScope(expressionBlock.getEnclosingScope());
		if (expr.accept(this) == null){
			return null;
		}

		return true;
	}

	private Object visitLocation(Location location){
		if (location instanceof ArrayLocation){
			ArrayLocation arrayLocation = (ArrayLocation) location;
			arrayLocation.setEnclosingScope(location.getEnclosingScope());
			if (arrayLocation.accept(this) == null){
				return null;
			}
		}
		else{
			VariableLocation variableLocation = (VariableLocation) location;
			variableLocation.setEnclosingScope(location.getEnclosingScope());
			if (variableLocation.accept(this) == null){
				return null;
			}
		}

		return true;
	}


	private Object visitMethod(Method method, boolean isStatic) {
		method.setOrder(1);
		try {
			method.getType().setEnclosingScope(method.getEnclosingScope());
			Type retValueType = TypeTable.getType(method.getType());
			MethodSymbolTable mst = new MethodSymbolTable(method.getName(), method.getEnclosingScope(), retValueType, isStatic);
			List<Formal> formals =  method.getFormals();
			for (Formal currentFormal: formals){
				mst.insert(currentFormal);
				currentFormal.setEnclosingScope(mst);
				if (currentFormal.accept(this) == null){
					//TODO semantic error;
					return null;
				}
			}

			List<Statement> statements = method.getStatements();
			for (Statement currentStmt: statements){
				currentStmt.setEnclosingScope(mst);
				currentStmt.setInsideALoop(false);
				Object result = currentStmt.accept(this);
				if (result == null){
					//TODO semantic error;
					return null;
				}
			}
			return mst;
		} 
		catch (SemanticError e) {
			System.err.println(e);
			return null;
		}
	}

	private SymbolTable getClassSymbolTable(ClassSymbol classSymbol){
		
		List<String> classHierarchy = new ArrayList<String>();

		ClassType currentType = (ClassType) classSymbol.getType();
		classHierarchy.add(currentType.getName());
		String superClassName = currentType.getSuperClassName();

		while (superClassName != null){
			classHierarchy.add(superClassName);
			currentType = currentType.getSuperClass();
			superClassName = currentType.getSuperClassName();
		}

		SymbolTable currentSymbolTable = gst;
		for (int i = classHierarchy.size() - 1; i >= 0; i--){
			currentSymbolTable = currentSymbolTable.getChild(classHierarchy.get(i));
		}
		return currentSymbolTable;
	}

}